package org.yihang.util;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;
import org.yihang.annotation.*;
import org.yihang.util.Meta.Dialect;
import org.yihang.util.Meta.Method;
import org.yihang.util.Meta.NameStrategy;
import org.yihang.util.Meta.Structure;

import freemarker.cache.ClassTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

/**
 * mapper 生成器
 * <p>
 * 实体类:
 * </p>
 * <p>
 * <pre>
 * package com.xingxue.entity;
 *
 * public class Product {
 * 	&#064;FindBy
 * 	&#064;DeleteBy
 * 	private long id;
 * 	&#064;FindByVo(like = true)
 * 	private String name;
 * }
 * </pre>
 * <p>
 * 执行:
 * </p>
 * <p>
 * <pre>
 * new MapperGenerator.Builder().mapperPackage("com.xingxue.mapper").build().process(Product.class);
 * </pre>
 * <p>
 * 则会生成:
 * </p>
 * <ul>
 * <li>com/xingxue/mapper/ProductMapper.java</li>
 * <li>com/xingxue/mapper/ProductMapper.xml</li>
 * <li>com/xingxue/entity/vo/ProductVo.java</li>
 * </ul>
 */
public class MapperGenerator {

    private MapperGenerator() {
    }

    private Set<Meta.Method> methods = new LinkedHashSet<>();
    private String mapperPackage;
    private String voPackage;
    private NameStrategy nameStrategy;
    private Dialect dialect;
    private Structure structure;
    private String outputDir;
    private String idProperty;

    public static class Builder {
        private MapperGenerator semi;

        public Builder() {
            semi = new MapperGenerator();
            semi.methods.addAll(Arrays.asList(Method.values()));
            semi.nameStrategy = NameStrategy.CamelUnderscore;
            semi.dialect = Dialect.MySQL;
            semi.structure = Structure.Maven;
            semi.outputDir = System.getProperty("user.dir");
            semi.idProperty = "id";
        }

        /**
         * 应当包含的模板方法
         *
         * @param methods
         * @return
         */
        public Builder include(Method... methods) {
            if (methods != null && methods.length > 0) {
                semi.methods.clear();
                semi.methods.addAll(Arrays.asList(methods));
            }
            return this;
        }

        /**
         * 应当排除的模板方法
         *
         * @param methods
         * @return
         */
        public Builder exclude(Method... methods) {
            if (methods != null && methods.length > 0) {
                semi.methods.removeAll(Arrays.asList(methods));
            }
            return this;
        }

        /**
         * 设置命名策略，默认按`驼峰-下划线`策略进行转换
         *
         * @param nameStrategy
         * @return
         */
        public Builder nameStrategy(NameStrategy nameStrategy) {
            semi.nameStrategy = nameStrategy;
            return this;
        }

        /**
         * 设置方言，默认为`MySQL`
         *
         * @param dialect
         * @return
         */
        public Builder dialect(Dialect dialect) {
            semi.dialect = dialect;
            return this;
        }

        /**
         * 设置输出的目录结构，默认为`Maven`方式
         *
         * @param structure
         * @return
         */
        public Builder structure(Structure structure) {
            semi.structure = structure;
            return this;
        }

        /**
         * 输出路径，默认为当前项目路径
         *
         * @param outputDir
         * @return
         */
        public Builder outputDir(String outputDir) {
            semi.outputDir = outputDir;
            return this;
        }

        /**
         * 设置id属性名，默认为`id`; 如果没有指定，去查找加了@Id注解的值
         *
         * @param idProperty
         * @return
         */
        public Builder idProperty(String idProperty) {
            semi.idProperty = idProperty;
            return this;
        }

        /**
         * mapper 接口和xml输出的包名(必须)
         *
         * @param mapperPackage
         * @return
         */
        public Builder mapperPackage(String mapperPackage) {
            semi.mapperPackage = mapperPackage;
            return this;
        }

        /**
         * vo对象的包名，默认为 `实体类包名.vo`
         *
         * @param voPackage
         * @return
         */
        public Builder voPacakge(String voPackage) {
            semi.voPackage = voPackage;
            return this;
        }

        public MapperGenerator build() {
            return semi;
        }
    }

    public void process(Class<?> c) {
        final Map<String, Object> map = new HashMap<>();
        map.put("dialect", dialect.toString().toLowerCase());
        map.put("package", this.mapperPackage);
        if (this.voPackage == null) {
            this.voPackage = c.getPackage().getName() + ".vo";
        }
        map.put("vopackage", this.voPackage);
        map.put("save", this.methods.contains(Method.SAVE));
        map.put("update", this.methods.contains(Method.UPDATE));
        map.put("deleteBy", this.methods.contains(Method.DELETE_BY));
        map.put("findBy", this.methods.contains(Method.FIND_BY));
        map.put("findAll", this.methods.contains(Method.FIND_ALL));
        map.put("findCount", this.methods.contains(Method.FIND_COUNT));
        map.put("findByVo", this.methods.contains(Method.FIND_BY_VO));

        map.put("mapper", c.getSimpleName() + "Mapper");
        map.put("class", c.getName()); // 类全名
        String class_s = c.getSimpleName();
        map.put("class_s", class_s); // 类简名
        map.put("class_ss", Character.toLowerCase(c.getSimpleName().charAt(0)) + c.getSimpleName().substring(1)); // 类名作为参数时名称
        map.put("voclass", this.voPackage + "." + c.getSimpleName() + "Vo");
        map.put("voclass_s", c.getSimpleName() + "Vo");
        Field[] fields = c.getDeclaredFields();

        // 处理id
        Optional<Field> idOptional = Arrays.stream(fields)
                .filter(f -> f.getName().equals(idProperty) || f.getAnnotation(Id.class) != null).findFirst();
        if (!idOptional.isPresent()) {
            throw new RuntimeException("no @Id field!");
        }
        Field id = idOptional.get();
        String id_column = nameStrategy.columnName(id);
        String id_property = id.getName();
        String by_id = id_column + "=#{" + id_property + "}";

        map.put("id_column", id_column);
        map.put("by_id", by_id);
        map.put("id_class", id.getType().getCanonicalName());
        map.put("id_class_boxed", getBoxedType(id.getType()).getCanonicalName());
        map.put("id_property", id_property);

        // 处理select子句
        map.put("select_columns",
                Arrays.stream(fields)
                        .filter(f -> !Modifier.isTransient(f.getModifiers())).map(f -> nameStrategy.columnName(f)).collect(Collectors.joining(",")));

        // 处理表名
        String table_name = nameStrategy.tableName(c);
        map.put("table_name", table_name);

        // 处理save
        if (Boolean.TRUE.equals(map.get("save"))) { // 生成save
            if (dialect.equals(Dialect.MySQL)) {
                map.put("save_columns", Arrays.stream(fields).filter(f -> !f.equals(id) && !Modifier.isTransient(f.getModifiers()))
                        .map(f -> nameStrategy.columnName(f)).collect(Collectors.joining(",")));
                map.put("save_properties", Arrays.stream(fields).filter(f -> !f.equals(id) && !Modifier.isTransient(f.getModifiers()))
                        .map(f -> "#{" + f.getName() + "}").collect(Collectors.joining(",")));
            } else if (dialect.equals(Dialect.ORACLE)) {
                map.put("save_columns",
                        Arrays.stream(fields).filter(f -> !Modifier.isTransient(f.getModifiers())).map(f -> nameStrategy.columnName(f)).collect(Collectors.joining(",")));
                String properties = Arrays.stream(fields).filter(f -> !Modifier.isTransient(f.getModifiers())).map(f -> {
                    if (f.equals(id)) {
                        return table_name + "_seq.nextval,";
                    } else {
                        return "#{" + f.getName() + "}";
                    }
                }).collect(Collectors.joining(","));
                map.put("save_properties", properties);
            }
        }

        // 处理update
        if (Boolean.TRUE.equals(map.get("update"))) {
            String update_fields = Arrays.stream(fields).filter(f -> !f.equals(id) && !Modifier.isTransient(f.getModifiers())).map(f -> {
                String column = nameStrategy.columnName(f);
                String property = f.getName();
                return "<if test=\"" + property + "!=" + getDefaultValue(f.getType()) + "\">" + column + "=#{"
                        + property + "},</if>";
            }).collect(Collectors.joining());
            map.put("update_fields", update_fields);
        }

        // 处理 findBy
        if (Boolean.TRUE.equals(map.get("findBy"))) {
            List<ByProperty> list = new ArrayList<>();
            Arrays.stream(fields).forEach(f -> {
                String prop = f.getName();
                String type = f.getType().getCanonicalName();
                String boxedType = getBoxedType(f.getType()).getCanonicalName();
                String column = nameStrategy.columnName(f);
                FindBy anno = f.getAnnotation(FindBy.class);
                if (anno != null) {
                    if (!anno.onlyMulti()) {
                        String by = column + "=#{" + prop + "}";
                        if (f.equals(id) || anno.unique()) {
                            list.add(new ByProperty("findBy" + firstUpper(prop), prop, type, type, class_s, by));
                        } else {
                            list.add(new ByProperty("findBy" + firstUpper(prop), prop, type, type, "List<" + class_s + ">", by));
                        }
                    }
                    if (anno.onlyMulti() || anno.withMulti()) {
                        String by = column
                                + " in <foreach open=\"(\" close=\")\" separator=\",\" collection=\"list\" item=\"it\">#{it}</foreach>";
                        list.add(new ByProperty("findBy" + firstUpper(prop) + "s", prop, "list",
                                "List<" + boxedType + ">", "List<" + class_s + ">", by));
                    }
                }
            });
            map.put("findby_query", list);
        }

        // 处理 deleteBy
        if (Boolean.TRUE.equals(map.get("deleteBy"))) {
            List<ByProperty> list = new ArrayList<>();
            Arrays.stream(fields).forEach(f -> {
                String prop = f.getName();
                String type = f.getType().getCanonicalName();
                String boxedType = getBoxedType(f.getType()).getCanonicalName();
                String column = nameStrategy.columnName(f);
                DeleteBy anno = f.getAnnotation(DeleteBy.class);
                if (anno != null) {
                    if (!anno.onlyMulti()) {
                        String by = column + "=#{" + prop + "}";
                        list.add(new ByProperty("deleteBy" + firstUpper(prop), prop, type, type, "void", by));
                    }
                    if (anno.onlyMulti() || anno.withMulti()) {
                        String by = column
                                + " in <foreach open=\"(\" close=\")\" separator=\",\" collection=\"list\" item=\"it\">#{it}</foreach>";
                        list.add(new ByProperty("deleteBy" + firstUpper(prop) + "s", prop, "list",
                                "List<" + boxedType + ">", "void", by));
                    }
                }
            });
            map.put("deleteby_query", list);
        }

        // 处理 findAll
        if (Boolean.TRUE.equals(map.get("findAll"))) {

        }

        // 处理 findByVo
        if (Boolean.TRUE.equals(map.get("findByVo"))) {
            Optional<Field> op = Arrays.stream(fields)
                    .filter(f -> !f.equals(id) && f.getAnnotation(FindByVo.class) != null).findAny();
            Optional<Field> importDateTimeFormat = Arrays.stream(fields)
                    .filter(f -> f.getAnnotation(DateTimeFormat.class) != null).findAny();
            Optional<Field> importNumberFormat = Arrays.stream(fields)
                    .filter(f -> f.getAnnotation(NumberFormat.class) != null).findAny();
            StringBuilder conditions = new StringBuilder(512);
            List<VoProperty> list = new ArrayList<>();
            if (op.isPresent()) {
                conditions.append("<where>");
                Arrays.stream(fields).filter(f -> !f.equals(id)).forEach(f -> {
                    String column = nameStrategy.columnName(f);
                    String property = f.getName();
                    FindByVo cond = f.getAnnotation(FindByVo.class);
                    if (cond != null) {
                        // 处理比较条件的默认值
                        String d = getDefaultValue(getBoxedType(f.getType()));
                        if (!FindByVo.DEFAULT.equals(cond.nullValue())) {
                            d = cond.nullValue();
                        }
                        DateTimeFormat stf = f.getAnnotation(DateTimeFormat.class);
                        NumberFormat nf = f.getAnnotation(NumberFormat.class);
                        List<String> annos = new ArrayList<>();
                        if (stf != null) {
                            annos.add("@DateTimeFormat(pattern=\"" + stf.pattern() + "\",iso=" + stf.iso() + ")");
                        }
                        if (nf != null) {
                            annos.add("@NumberFormat(pattern=\"" + nf.pattern() + "\",style=" + nf.style() + ")");
                        }
                        String anno = annos.stream().collect(Collectors.joining(" "));

                        String sqlpart = cond.voType().getSqlPart(new SqlProperty(column, property, d));
                        conditions.append(sqlpart);
                        list.addAll(cond.voType().getVoProperty(f, property, anno));
                    }
                });
                conditions.append("</where>");
            }
            map.put("importDateTimeFormat", importDateTimeFormat.isPresent());
            map.put("importNumberFormat", importNumberFormat.isPresent());
            map.put("conditions", conditions.toString());
            map.put("voProperties", list);
        }

        // 处理@Middle
        Middle[] middles = c.getAnnotationsByType(Middle.class);
        List<MiddleProperty> middleProperties = new ArrayList<>();
        for (Middle m : middles) {
            middleProperties.add(new MiddleProperty(m.table(), m.oppositeClass(), m.title(), m.ownColumn(), m.oppositeColumn()));
        }
        map.put("middleProperties", middleProperties);


        // 处理resultMap
        StringBuilder result_map = new StringBuilder(255);
        {
            String column = nameStrategy.columnName(id);
            String property = id.getName();
            result_map.append("<id column=\"").append(column).append("\" property=\"").append(property).append("\" />")
                    .append(System.lineSeparator());
        }
        String temp = Arrays.stream(fields).filter(f -> !f.equals(id) && !Modifier.isTransient(f.getModifiers())).map(f -> {
            String column = nameStrategy.columnName(f);
            String property = f.getName();
            return "\t\t<result column=\"" + column + "\" property=\"" + property + "\" />";
        }).collect(Collectors.joining(System.lineSeparator()));
        result_map.append(temp);
        map.put("result_map", result_map.toString());

        try {
            Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
            cfg.setTemplateLoader(new ClassTemplateLoader(MapperGenerator.class, "/template"));
            cfg.setDefaultEncoding("UTF-8");
            buildMapper(cfg, map);
            if (Boolean.TRUE.equals(map.get("findByVo"))) {
                if (voPackage == null) {
                    throw new RuntimeException("voPackage must specific!");
                }
                buildVo(cfg, map);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Class<?> getBoxedType(Class<?> type) {
        if (type == int.class) {
            return Integer.class;
        } else if (type == short.class) {
            return Short.class;
        } else if (type == byte.class) {
            return Byte.class;
        } else if (type == long.class) {
            return Long.class;
        } else if (type == char.class) {
            return Character.class;
        } else if (type == float.class) {
            return Float.class;
        } else if (type == double.class) {
            return Double.class;
        } else if (type == boolean.class) {
            return Boolean.class;
        } else {
            return type;
        }
    }

    private void buildVo(Configuration cfg, Map<String, Object> map)
            throws TemplateException, IOException {
        String path = this.structure.javaOutputDir(this.outputDir, this.voPackage);
        new File(path).mkdirs();
        Template t_vo = cfg.getTemplate("vo.ftl");
        t_vo.process(map, new OutputStreamWriter(new FileOutputStream(path + map.get("class_s") + "Vo.java")));
    }

    private void buildMapper(Configuration cfg, final Map<String, Object> map)
            throws TemplateException, IOException {
        String path = this.structure.javaOutputDir(this.outputDir, this.mapperPackage);
        new File(path).mkdirs();
        Template t_interface = cfg.getTemplate("mapper_interface.ftl");
        t_interface.process(map, new OutputStreamWriter(new FileOutputStream(path + map.get("mapper") + ".java")));

        Template t_xml = cfg.getTemplate("mapper_xml.ftl");
        path = this.structure.xmlOutputDir(this.outputDir, this.mapperPackage);
        new File(path).mkdirs();
        t_xml.process(map, new OutputStreamWriter(new FileOutputStream(path + map.get("mapper") + ".xml")));
    }

    // 获得属性的get方法名
    public static String get(String property) {
        return "get" + firstUpper(property);
    }

    // 获得属性的set方法名
    public static String set(String property) {
        return "set" + firstUpper(property);
    }

    // 获得属性的is方法名
    public static String is(String property) {
        return "is" + firstUpper(property);
    }

    public static String firstUpper(String property) {
        return Character.toUpperCase(property.charAt(0)) + property.substring(1);
    }

    private static String getDefaultValue(Class<?> c) {
        String v = DEFAULT_VALUES.get(c);
        return (v == null) ? "null" : v;
    }

    private static Map<Class<?>, String> DEFAULT_VALUES = new HashMap<>();

    static {
        DEFAULT_VALUES.put(int.class, "0");
        DEFAULT_VALUES.put(short.class, "0");
        DEFAULT_VALUES.put(byte.class, "0");
        DEFAULT_VALUES.put(long.class, "0");
        DEFAULT_VALUES.put(char.class, "0");
        DEFAULT_VALUES.put(float.class, "0");
        DEFAULT_VALUES.put(double.class, "0");
        DEFAULT_VALUES.put(boolean.class, "false");
    }

}
